##########################################################################
#                                                                        #
#  Copyright (C) 2014      NEC Deutschland GmbH, NEC HPC Europe.         #
#                                                                        #
#  These coded instructions, statements, and computer programs  contain  #
#  unpublished  proprietary  information of NEC HPC Europe, and are      #
#  are protected by Federal copyright law.  They  may  not be disclosed  #
#  to  third  parties  or copied or duplicated in any form, in whole or  #
#  in part, without the prior written consent of NEC HPC Europe.         #
#                                                                        #
##########################################################################
#
# $Id$
#

try:
    from collections import OrderedDict
except:
    from ordereddict import OrderedDict
from threading import Lock


__all__ = ["LRUCache"]


class LRUCache(object):
    """
    A LRU (least recently used) cache built on top of an ordered dict.
    Access to the dict is protected by a lock.

    version is a number or a string, that can be chacked to find out if the cached value is still valid.
    It must be returned by the getter() function along with the value.
    getter() must return a tuple of (value, version).
    validator() checks if a version is valid or not.
    """
    def __init__( self, getter=None, getter_args=[], validator=None, validator_args=[], maxlen=1000 ):
        self.cache = OrderedDict()
        self.getter = getter
        self.getter_args = getter_args
        self.is_valid = validator
        self.validator_args = validator_args
        self.maxlen = maxlen
        self.lock = Lock()
        self.hit = 0
        self.miss = 0
        self.invalid = 0

    def get( self, key ):
        value = None
        version = None
        if not isinstance( key, basestring ):
            key = str( key )
        with self.lock:
            if key in self.cache:
                self.hit += 1
                value, version = self.cache.pop(key)
                if self.is_valid( key, version, *self.validator_args ):
                    self.cache[key] = (value, version)
                else:
                    self.invalid += 1
                    version = None
            else:
                self.miss += 1
        if version is None:
            value, version = self.getter( key, *self.getter_args )
            if version is None:
                # getter failed, so return None and don't touch the cache
                return None
            with self.lock:
                if len( self.cache ) == self.maxlen:
                    _val, _ver = self.cache.popitem( last=False )
                self.cache[key] = (value, version)
        return value

    def stats( self, *args ):
        if args and args[0] == "show":
            out = [(key, val[1]) for key, val in self.cache.items()]
        else:
            out = {"maxlen": self.maxlen, "size": len(self.cache), "cache_hits": self.hit,
                   "cache_misses": self.miss, "invalidated": self.invalid}
        return out


#
# Some basic self-test
#
if __name__ == "__main__":
    from random import seed, randint
    from time import time

    seed()

    def get_num(k):
        k = int(k)
        val = randint( k - 10, k + 10 )
        ver = time() + 0.001
        return val, ver

    def is_valid( ver ):
        return time() < ver

    cache = LRUCache( getter=get_num, validator=is_valid, maxlen=100 )

    for i in xrange(10000):
        j = str(randint(1, 1000))
        v = cache.get( j )
    print cache.stats()
